﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.ServiceModel;
using System.Security.Principal;
using System.Xml.Schema;
using Microsoft.ResourceManagement.Client;
using Microsoft.ResourceManagement.ObjectModel;
using Microsoft.ResourceManagement.ObjectModel.ResourceTypes;
using Microsoft.ResourceManagement.Client.WsTransfer;
using Microsoft.ResourceManagement.Client.WsEnumeration;
using System.ServiceModel.Channels;
using System.Net;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Globalization;

namespace Quest.FIMPowerShellSnapin
{
    public class FIMPSClient
    {
        private const String _defaultResourceManagementPort = "5725";
        private const String _defaultSTSPort = "5726";

        private MexClient _mexClient;
        private WsTransferClient _wsTransferClient;
        private WsTransferFactoryClient _wsTransferFactoryClient;
        private WsEnumerationClient _wsEnumerationClient;
        private RmResourceFactory _resourceFactory;
        private RmRequestFactory _requestFactory;
        private PSHost _pSHost;
        private PSCredential _pSCreds;
        private string _fIMServer;
        private string _fIMPort;
        private string[] _defaultAttrNames;
        private bool _schemaCached;
        private SchemaCache _schemaCache;

        /// <summary>
        /// This class contains the names of the FIM web service endpoints. They are copied from 
        /// Microsoft.ResourceManagement.Client.Constants.Endpoint, which is unfortunately marked internal, which makes
        /// them inaccessible.
        /// </summary>
        internal class EndpointNames
        {
            private EndpointNames() { }
            public const String MEX = "ResourceManagementService/MEX";
            public const String Resource = "ResourceManagementService/Resource";
            public const String ResourceFactory = "ResourceManagementService/ResourceFactory";
            public const String Enumeration = "ResourceManagementService/Enumeration";
            public const String Alternate = "ResourceManagementService/Alternate";
        }

        public FIMPSClient(string fIMserverParam, PSCredential creds, PSHost pSHost)
        {
            // Save reference to PS host environment
            _pSHost = pSHost;
            _pSCreds = creds;

            // Separate FIM server name and port if any, and save
            int colonPosition = fIMserverParam.IndexOf(':');
            if (colonPosition < 0)
            {
                _fIMServer = fIMserverParam.ToLower();
                _fIMPort = _defaultResourceManagementPort;
            }
            else
            {
                _fIMServer = fIMserverParam.Substring(0, colonPosition);
                _fIMPort = fIMserverParam.Substring(colonPosition + 1);
                /*!!! check _fIMPort for numeric value*/
            }

            _mexClient = CreateMexClient();
            _wsTransferClient = CreateTransferClient();
            _wsTransferFactoryClient = CreateTransferFactoryClient();
            _wsEnumerationClient = CreateEnumerationClient();

            _resourceFactory = new RmResourceFactory();
            _requestFactory = new RmRequestFactory();
            _schemaCache = new SchemaCache();
        }

        /// <summary>
        /// Refreshes the metadata from the service.
        /// </summary>
        public XmlSchemaSet RefreshSchema()
        {
            XmlSchemaSet metadata = _mexClient.Get();

            lock (_requestFactory)
            {
                _requestFactory = new RmRequestFactory(metadata);
            }
            lock (_resourceFactory)
            {
                _resourceFactory = new RmResourceFactory(metadata);
            }
            lock (_schemaCache)
            {
                _schemaCache = new SchemaCache(metadata);
            }
            
            _schemaCached = true;

            return metadata;
        }

        public SchemaCache Schema
        {
            get
            {
                if(!_schemaCached)
                {
                    RefreshSchema();
                }
                return _schemaCache;
            }
        }

        #region ResourceTransfer
        /// <summary>
        /// Retrieves the object with the given ObjectId
        /// </summary>
        /// <param name="objectId">The ObjectId of the requested object.</param>
        /// <returns>The object or null if not found</returns>
        /// <exception cref="System.ServiceModel.FaultException">System.ServiceModel.FaultException thrown when failures occur.</exception>
        public RmResource Get(RmReference objectId)
        {
            return Get(objectId, null, null);
        }

        /// <summary>
        /// Retrieves the representation of an object with the given ObjectId in the given culture.
        /// </summary>
        /// <param name="objectId">The ObjectId of the requested object.</param>
        /// <param name="culture">The requested culture representation of the object.</param>
        /// <returns>The object or null if not found.</returns>
        public RmResource Get(RmReference objectId, CultureInfo culture)
        {
            return Get(objectId, culture, null);
        }

        /// <summary>
        /// Retrieves the object and the specified attributes with the given ObjectId.
        /// </summary>
        /// <param name="objectId">The ObjectId of the requested object.</param>
        /// <param name="attributes">The list of attributes on the object to return.</param>
        /// <returns></returns>
        public RmResource Get(RmReference objectId, String[] attributes)
        {
            return Get(objectId, null, attributes);
        }

        /// <summary>
        /// Retrieves the object and the specified attributes with the given ObjectId and in the specified culture.
        /// </summary>
        /// <param name="objectId">The ObjectId of the requested object.</param>
        /// <param name="culture">The requested culture representation of the object.</param>
        /// <param name="attributes">The list of attributes on the object to return.</param>
        /// <returns>The requested object, or null if it is not found.</returns>
        public RmResource Get(RmReference objectId, CultureInfo culture, String[] attributes)
        {
            GetRequest request = _requestFactory.CreateGetRequest(objectId, culture, attributes);
            GetResponse response = _wsTransferClient.Get(request);
            return _resourceFactory.CreateResource(response);
        }

        #endregion

        #region Enumeration
        /// <summary>
        /// Returns an enumerator that can traverse all objects matching the given filter.
        /// </summary>
        /// <param name="filter">The XPath filter of which objects to select.</param>
        /// <returns>An enumerator object which can be consumed in foreach statements.</returns>
        public IEnumerable<RmResource> Enumerate(String filter)
        {
            return this.Enumerate(filter, new string[] { });
        }

        /// <summary>
        /// Returns an enumerator that can traverse all objects matching the given filter.
        /// 
        /// Each object only contains the specified attributes.
        /// </summary>
        /// <param name="filter">The XPath filter of which objects to select.</param>
        /// <param name="attributes">A list of attribute names to include in returned objects.</param>
        /// <returns>An enumerator object which can be consumed in foreach statements.</returns>
        public IEnumerable<RmResource> Enumerate(String filter, String[] attributes)
        {
            if (String.IsNullOrEmpty(filter))
            {
                throw new ArgumentNullException("filter");
            }

            return _requestFactory.CreateEnumeration(_wsEnumerationClient, _resourceFactory, filter, attributes);
        }
        #endregion

        #region Create
        /// <summary>
        /// Creates the given resource and returns its ObjectId.
        /// This method does not set the ObjectId of newResource.
        /// </summary>
        /// <param name="newResource">The resource to create.</param>
        /// <returns>The ObjectId of the resource.</returns>
        public RmReference Create(RmResource newResource)
        {
            if (newResource == null)
                throw new ArgumentNullException("newResource");
            CreateRequest request = _requestFactory.CreateCreateRequest(newResource);
            CreateResponse response = _wsTransferFactoryClient.Create(request);
            try
            {
                RmReference reference = new RmReference(response.ResourceCreated.EndpointReference.ReferenceProperties.ResourceReferenceProperty.Value);
                newResource.ObjectID = reference;
                return reference;
            }
            catch (NullReferenceException)
            {
                return new RmReference();
            }
            catch (FormatException)
            {
                return new RmReference();
            }
        }
        #endregion

        #region Put
        /// <summary>
        /// Saves changes made to an object recorded by the transaction to the service.
        /// </summary>
        /// <param name="transaction">The transaction object which recorded changes made to a Resource object.</param>
        /// <returns>True upon successful operation.</returns>
        public bool Put(RmResourceChanges transaction)
        {
            if (transaction == null)
            {
                throw new ArgumentNullException("transaction");
            }

            PutRequest request = _requestFactory.CreatePutRequest(transaction);
            PutResponse response = _wsTransferClient.Put(request);
            if (response == null)
                return false;
            else
                return true;
        }        
        #endregion

        #region Delete
        /// <summary>
        /// Deletes the object with the given ObjectId.
        /// </summary>
        /// <param name="objectId">The ObjectId of the object to delete.</param>
        /// <returns>True upon successful deletion.</returns>
        public bool Delete(RmReference objectId)
        {
            DeleteRequest request = _requestFactory.CreateDeleteRequest(objectId);
            DeleteResponse response = _wsTransferClient.Delete(request);
            if (response == null)
                return false;
            else
                return true;
        }
        #endregion

        /// <summary>
        /// Returns true if the schema has been cached for this client.
        /// </summary>
        public bool SchemaCached
        {
            get
            {
                return _schemaCached;
            }
        }

        private MexClient CreateMexClient()
        {
            EndpointAddress remoteAddress = new EndpointAddress(CreateServiceEndpointURI(EndpointNames.MEX), CreateFIMSPNIdentity());
            WSHttpBinding binding = new WSHttpBinding(SecurityMode.None);
            binding.MaxReceivedMessageSize = Int32.MaxValue;

            return new MexClient(binding, remoteAddress);
        }

        private WsTransferClient CreateTransferClient()
        {
            EndpointAddress remoteAddress = new EndpointAddress(CreateServiceEndpointURI(EndpointNames.Resource), CreateFIMSPNIdentity());

            System.ServiceModel.WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
            binding.Security.Message.EstablishSecurityContext = false;
            binding.MaxReceivedMessageSize = Int32.MaxValue;

            WsTransferClient client = new WsTransferClient(binding, remoteAddress);
            client.ClientCredentials.Windows.ClientCredential = _pSCreds.GetNetworkCredential();
            return client;
        }

        private WsTransferFactoryClient CreateTransferFactoryClient()
        {
            EndpointAddress remoteAddress = new EndpointAddress(CreateServiceEndpointURI(EndpointNames.ResourceFactory), CreateFIMSPNIdentity());

            System.ServiceModel.WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
            binding.Security.Message.EstablishSecurityContext = false;
            binding.MaxReceivedMessageSize = Int32.MaxValue;

            WsTransferFactoryClient client = new WsTransferFactoryClient(binding, remoteAddress);
            client.ClientCredentials.Windows.ClientCredential = _pSCreds.GetNetworkCredential();
            return client;
        }

        private WsEnumerationClient CreateEnumerationClient()
        {
            EndpointAddress remoteAddress = new EndpointAddress(CreateServiceEndpointURI(EndpointNames.Enumeration), CreateFIMSPNIdentity());

            System.ServiceModel.WSHttpBinding binding = new WSHttpBinding(SecurityMode.Message);
            binding.Security.Message.EstablishSecurityContext = false;
            binding.MaxReceivedMessageSize = Int32.MaxValue;

            WsEnumerationClient client = new WsEnumerationClient(binding, remoteAddress);
            client.ClientCredentials.Windows.ClientCredential = _pSCreds.GetNetworkCredential();
            return client;
        }

        private Uri CreateServiceEndpointURI(string serviceEndpointName)
        {
            return new Uri(String.Format("http://{0}:{1}/{2}", _fIMServer, _fIMPort, serviceEndpointName));
        }

        EndpointIdentity CreateFIMSPNIdentity()
        {
            return EndpointIdentity.CreateSpnIdentity("IdentityManagementService/" + _fIMServer);
        }
    }
}
